package com.javaspeak.concurrency.lock.custom.reentrant;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.junit.Test;
import org.junit.internal.TextListener;
import org.junit.runner.JUnitCore;

/**
 * @author John Dickerson
 */
public class ReEntrantLockTest {

    private List<Future<CounterValue>> startTest(
            int numberThreads,
                int timeoutMilliseconds,
                    ExecutorService executorService )
                        throws InterruptedException {

        Counter counter = new Counter();

        Collection<IncrementCounterCallable> callables =
            new ArrayList<IncrementCounterCallable>();

        for ( int i=0; i<numberThreads; i++ ){

            callables.add(
                 new IncrementCounterCallable( counter, timeoutMilliseconds ) );
        }

        List<Future<CounterValue>> counterValueFutures =
            executorService.invokeAll( callables, 20, TimeUnit.SECONDS );

        return counterValueFutures;
    }

    /**
     * This test starts 10 threads which compete to call the doubleIncrement(..)
     * method on the counter.
     * <p>
     * A random delay has been added to the implementation of the
     * doubleIncrement(..) method to simulate contention.
     * <p>
     * The renentrant implementation in the ReEntrant class keeps a tab on how
     * many time the thread has acquired the lock before releasing it.
     * Only when the thread holding the lock has called releaseLock(..) the
     * same number of times as it called aquireLock(..) will it release the
     * lock on the monitor and let other threads take the lock.
     * <p>
     * The renentrant implementation will only allow a thread to take the lock a
     * second time if it is the thread already holding the lock.  It checks which
     * thread has the locks using:
     * <p>
     * Thread currentThread = Thread.currentThread();
     * <p>
     * The ReEntrantLock implementation allows the acquire method to be called
     * several times consequentively by the same thread before calling
     * releaseLock.
     * <p>
     * With a lock implementation that was not reentrant this would not be
     * allowed.
     * <p>
     * The ReEntrantLock implementation allows a method using the lock such
     * as the Counter (in this example) to call another method internally
     * which also has the lock.
     * <p>
     * This reentrant lock has been implemented using wait and notify and
     * synchronized blocks.
     *
     * @throws TimeoutException
     * @throws ExecutionException
     * @throws InterruptedException
     */
    @Test
    public void testReEntrantLock()
        throws TimeoutException, ExecutionException, InterruptedException {

        int numberThreads = 10;
        int timeoutMilliseconds = 20000;

        ExecutorService executorService =
            Executors.newFixedThreadPool( numberThreads );

        Set<CounterValue> counterValues =
            new TreeSet<CounterValue>( new CounterValueComparator() );

        List<Future<CounterValue>> counterValueFutures =
            startTest( numberThreads, timeoutMilliseconds, executorService );

        for ( Future<CounterValue> counterValueFuture : counterValueFutures ){

            CounterValue counterValue = counterValueFuture.get();
            counterValues.add( counterValue );
        }

        long nanoTimestampBefore = 0;
        long nanoTimestamp;

        int count = 2;

        for ( CounterValue counterValue : counterValues ){

            org.junit.Assert.assertEquals( count, counterValue.getValue() );

            nanoTimestamp = counterValue.getNanoTimestamp();
            count = count + 2;

            org.junit.Assert.assertTrue( nanoTimestamp > nanoTimestampBefore );
            nanoTimestampBefore = nanoTimestamp;

            System.out.println( counterValue.getNanoTimestamp() + " : " +
                    counterValue.getThreadName() + " : " +
                        counterValue.getValue() );
        }

        executorService.shutdown();
    }


    public static void main( String args[] ){

        JUnitCore junitCore = new JUnitCore();
        junitCore.addListener( new TextListener( System.out ) );
        junitCore.run( ReEntrantLockTest.class );
    }
}
